Skip to content

feat(storage): add storage zone and file commands with S3 credentials#98

Open
jamie-at-bunny wants to merge 8 commits into
mainfrom
storage-commands
Open

feat(storage): add storage zone and file commands with S3 credentials#98
jamie-at-bunny wants to merge 8 commits into
mainfrom
storage-commands

Conversation

@jamie-at-bunny

Copy link
Copy Markdown
Member

No description provided.

@changeset-bot

changeset-bot Bot commented Jun 18, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 7819dfe

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 6 packages
Name Type
@bunny.net/cli Minor
@bunny.net/cli-linux-x64 Minor
@bunny.net/cli-linux-arm64 Minor
@bunny.net/cli-darwin-x64 Minor
@bunny.net/cli-darwin-arm64 Minor
@bunny.net/cli-windows-x64 Minor

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

Comment thread packages/cli/README.md Outdated
Comment thread packages/cli/README.md Outdated
Comment thread packages/cli/README.md
Comment thread packages/cli/README.md

# Files: list, upload, download, delete (paths are relative to the zone root)
bunny storage file list my-zone
bunny storage file list my-zone images/

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

when dir doesn't exist it says Directory is empty. maybe a more accurate error message would be nicer. the current one implies the dir exists but empty.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we know either way here, unless something changed in the API recently. I discovered this while working on bunny-upload/file-manager. We could instead return No files found at "images/". The path may be empty or not exist though, wdyt?

Comment thread packages/cli/README.md
bunny storage file list my-zone images/
bunny storage file upload my-zone ./photo.png --to images/
bunny storage file upload my-zone ./photo.png --checksum --content-type image/png
bunny storage file download my-zone images/photo.png --out ./local.png

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this doesn't work for me. it gets stuck on downloading and blocks the process.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@amir-at-bunny can you check with the latest this works as you expect.

@jamie-at-bunny jamie-at-bunny marked this pull request as ready for review June 22, 2026 09:23
@bogdan-at-bunny

Copy link
Copy Markdown

@codex review

@greptile-apps greptile-apps Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Your free trial has ended. If you'd like to continue receiving code reviews, you can add a payment method here.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 2eb4f25d96

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

): string[] {
const primary = primaryCode?.toUpperCase();
const normalized = regions
.map((region) => region.trim().toUpperCase())

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Split comma-separated replication regions

The new examples use --replication LA,SG, but the parser passes that as one array element, and this normalization only trims/uppercases each element before validating it. As a result LA,SG is checked as a single region code and rejected as unknown, so the documented creation/update form fails unless users repeat the flag; split each entry on commas before validation or change the documented syntax.

Useful? React with 👍 / 👎.

};
}

if (output === "json") {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Add the requested domain before returning JSON

When bunny storage zones add ... --domain <host> --output json is used, --domain already made the pull zone get created, but this JSON branch returns before the custom-domain block below runs. The command therefore reports success while silently skipping the explicit domain the user asked to attach; perform the non-interactive hostname add before this return and include its result/error in the JSON payload.

Useful? React with 👍 / 👎.

Comment thread packages/cli/src/commands/storage/file/download.ts Outdated
Comment thread packages/cli/src/commands/storage/zone/hostnames/index.ts
Comment thread packages/cli/src/commands/storage/zone/add.ts Outdated
Comment thread packages/cli/src/commands/storage/zone/show.ts Outdated
Comment thread packages/cli/src/commands/storage/s3.ts Outdated
Comment thread packages/cli/src/commands/storage/zone/add.ts Outdated

@greptile-apps greptile-apps Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Your free trial has ended. If you'd like to continue receiving code reviews, you can add a payment method here.

@jamie-at-bunny jamie-at-bunny self-assigned this Jun 22, 2026

@greptile-apps greptile-apps Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Your free trial has ended. If you'd like to continue receiving code reviews, you can add a payment method here.

@jamie-at-bunny

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

To use Codex here, create a Codex account and connect to github.

@jamie-at-bunny

Copy link
Copy Markdown
Member Author

@greptile-apps re-review

@greptile-apps

greptile-apps Bot commented Jun 22, 2026

Copy link
Copy Markdown

Greptile Summary

This PR adds a comprehensive storage command group to the bunny CLI, covering storage zone lifecycle (add, list, show, update, remove), S3 credential export in multiple tool formats (rclone, aws, s3cmd, env), and file operations (list, upload, download, remove) backed by @bunny.net/storage-sdk.

  • Zone management (zones add/list/show/update/remove/credentials): interactive prompting with region/replication pickers, a toSafeStorageZone helper that strips passwords before any JSON output, and integrated pull-zone + custom-domain creation in the add flow.
  • File commands (files list/upload/download/remove): streaming I/O via FileSink for large objects, optional SHA-256 checksum on upload, and a --force bypass for the directory-delete confirmation.
  • S3 credentials (zones credentials): proper single-quote shell escaping for the env format (tested with metacharacters), and a maskSecret helper fix that now fully masks secrets ≤ 8 characters rather than leaking them.

Confidence Score: 5/5

The change is safe to merge. All new commands follow the established patterns in the codebase, passwords are stripped before JSON output, and the shell-quoting implementation for S3 env credentials is correctly tested.

The code is well-structured with no new defects found beyond what was already discussed in prior review threads. The maskSecret fix from the previous round has been correctly applied. The only new observations are a detached JSDoc comment in flow.ts and a double disk read when --checksum is used on upload — both cosmetic or minor performance concerns that don't affect correctness.

No files require special attention. The orphaned JSDoc in core/hostnames/flow.ts and the double file read in file/upload.ts are the only remaining items worth a second look.

Important Files Changed

Filename Overview
packages/cli/src/commands/storage/zone/add.ts New zones add command with interactive prompting, pull zone creation, and custom domain setup. Logic and overall flow look correct.
packages/cli/src/commands/storage/s3.ts S3 credential helpers with proper shell quoting for the env format. All four tool formats look correct.
packages/cli/src/commands/storage/zone/credentials.ts Shows/exports S3 credentials with secret masked in human mode and fully exposed in JSON/tool-format modes (intentional for scripting).
packages/cli/src/commands/storage/file/upload.ts Upload command with optional SHA-256 checksum. File is read twice when --checksum is passed — functionally correct but doubles disk I/O for large files.
packages/cli/src/commands/storage/files-api.ts Thin wrapper over @bunny.net/storage-sdk with correct region validation before connecting.
packages/cli/src/commands/storage/api.ts toSafeStorageZone strips passwords before JSON serialization; resolveStorageZone handles both numeric IDs and name lookups.
packages/cli/src/core/format.ts maskSecret fix correctly applied — short secrets ≤ 8 chars now return 8 bullets instead of partially revealing the value.
packages/cli/src/core/hostnames/flow.ts Extended with offerBunnyDnsThenSsl/setupHostname. An orphaned JSDoc block was left immediately above setupHostname during a refactor.
packages/cli/src/commands/storage/interactive.ts Zone picker that falls back from explicit ref → linked manifest → interactive select. Correctly blocks in non-TTY/JSON mode.
packages/cli/src/commands/storage/file/download.ts Streaming download with FileSink, properly creating parent directories and cleaning up partial files on error.

Reviews (3): Last reviewed commit: "apply greptile feedback" | Re-trigger Greptile

Comment on lines +149 to +167
});
replicationRegions = picked ?? [];
}
const replicationCodes = replicationRegions
? normalizeReplicationRegions(replicationRegions, mainRegion)
: [];

const spin = spinner("Creating storage zone...");
spin.start();
let created: StorageZoneModel | undefined;
try {
const { data } = await client.POST("/storagezone", {
body: {
Name: zoneName,
Region: mainRegion,
ReplicationRegions: replicationCodes.length ? replicationCodes : null,
},
});
created = data;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Unchecked API response on zone creation — also affects remove.ts and update.ts

client.POST (openapi-fetch) never throws on HTTP error responses; it returns { data: undefined, error: ... }. When the API rejects the request (e.g. 409 name conflict, 400 invalid region), data is undefined, created stays undefined, and execution continues — ultimately printing "Created storage zone <name>." or, in JSON mode, returning { "Name": "<name>", "PullZone": null, "CustomDomain": null } with no error signal. Callers have no way to distinguish a failure from success.

The same silent-success pattern appears in zone/remove.ts (the client.DELETE response is completely discarded) and zone/update.ts (the client.POST response is discarded), so a failed delete or update also logs "Deleted/Updated storage zone X." as if it succeeded.

Fix in Claude Code

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think this applies. We get {data,error} in our responses, and no throws.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looking at the actual code, the error from client.POST is never checked — only data is destructured:

const { data } = await client.POST("/storagezone", { ... });
created = data;

If the API returns a 409 or 400, data is undefined and error carries the failure — but error is silently dropped here. Execution continues past the try/finally, hits const zoneId = created?.Id (which is undefined), and eventually logs "Created storage zone <name>." as if it succeeded.

The fix would be to destructure and check error:

Suggested change
});
replicationRegions = picked ?? [];
}
const replicationCodes = replicationRegions
? normalizeReplicationRegions(replicationRegions, mainRegion)
: [];
const spin = spinner("Creating storage zone...");
spin.start();
let created: StorageZoneModel | undefined;
try {
const { data } = await client.POST("/storagezone", {
body: {
Name: zoneName,
Region: mainRegion,
ReplicationRegions: replicationCodes.length ? replicationCodes : null,
},
});
created = data;
const { data, error } = await client.POST("/storagezone", {
body: {
Name: zoneName,
Region: mainRegion,
ReplicationRegions: replicationCodes.length ? replicationCodes : null,
},
});
if (error) throw new UserError(String(error));
created = data;

Tip: You can customize Greptile's behavior for this repo with .greptile/rules.md and .greptile/config.json.

Comment thread packages/cli/src/core/format.ts
@jamie-at-bunny

Copy link
Copy Markdown
Member Author

We should also prompt before adding a replica that it cannot later be removed.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants